﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;

namespace SensorDiagnostics
{
    /// <summary>
    /// History graph control.
    /// </summary>
    public partial class HistoryGraph : UserControl
    {
        /// <summary>
        /// Width and height of each cell in the background grid in pixels.
        /// </summary>
        const int CELL_SIZE = 13;

        /// <summary>
        /// Number of data points to be rendered in the width of a single cell.
        /// </summary>
        const int POINTS_PER_CELL = 6;

        /// <summary>
        /// Number of pixels to be used by each data point.
        /// </summary>
        const int PIXELS_PER_POINT = CELL_SIZE / POINTS_PER_CELL;

        /// <summary>
        /// Collection of data points to be tracked by the graph.
        /// </summary>
        protected Dictionary<string, DataPoint> dataPoints = new Dictionary<string, DataPoint>();

        /// <summary>
        /// Timer used to update the graph.
        /// </summary>
        protected Timer updateTimer = new Timer();

        public HistoryGraph()
        {
            InitializeComponent();

            //
            // grid color is green by default
            //
            GridColor = Color.Green;

            //
            // default update frequency is 1 second
            //
            UpdateFrequency = 1000;

            //
            // initialize update timer
            //
            updateTimer.Enabled = false;
            updateTimer.Tick += new EventHandler(updateTimer_Tick);

            //
            // default render mode is fill
            //
            RenderMode = GraphRenderMode.Fill;

            //
            // enable double buffering to reduce flicker
            //
            DoubleBuffered = true;
        }

        /// <summary>
        /// Gets or sets the render mode.
        /// </summary>
        /// <value>The render mode.</value>
        public GraphRenderMode RenderMode
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the color of the grid.
        /// </summary>
        /// <value>The color of the grid.</value>
        public Color GridColor
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the update frequency in milliseconds.
        /// </summary>
        /// <value>The update frequency in milliseconds.</value>
        public Int32 UpdateFrequency
        {
            get
            {
                return updateTimer.Interval;
            }

            set
            {
                updateTimer.Interval = value;
            }
        }

        void updateTimer_Tick(object sender, EventArgs e)
        {
            Invalidate();
        }

        /// <summary>
        /// Adds a new data point.
        /// </summary>
        /// <param name="name">The name of the data point.</param>
        /// <param name="min">The minimum value of the range of values
        /// expected for this data point.</param>
        /// <param name="max">The maximum value of the range of values
        /// expected for this data point.</param>
        public void AddDataPoint(string name, double min, double max)
        {
            //
            // if datapoint already exists do nothing
            //
            if (dataPoints.ContainsKey(name))
                return;

            //
            // delegate to get a random unused color
            //
            Func<Color> getRandomUnusedColor = () =>
            {
                Random rnd = new Random();
                Color color = Color.FromArgb(rnd.Next(Byte.MaxValue), rnd.Next(Byte.MaxValue), rnd.Next(Byte.MaxValue));
                while(dataPoints.Any(dp => dp.Value.Color == color) == true)
                    color = Color.FromArgb(rnd.Next(Byte.MaxValue), rnd.Next(Byte.MaxValue), rnd.Next(Byte.MaxValue));
                return color;
            };

            AddDataPoint(name, min, max, getRandomUnusedColor());
        }

        /// <summary>
        /// Adds a new data point.
        /// </summary>
        /// <param name="name">The name of the data point.</param>
        /// <param name="min">The minimum value of the range of values
        /// expected for this data point.</param>
        /// <param name="max">The maximum value of the range of values
        /// expected for this data point.</param>
        /// <param name="color">The color to be used to represent the data point on the graph.</param>
        public void AddDataPoint(string name, double min, double max, Color color)
        {
            //
            // if datapoint already exists do nothing
            //
            if (dataPoints.ContainsKey(name))
                return;

            dataPoints.Add(name, new DataPoint
            {
                Name = name,
                Min = min,
                Max = max,
                Color = color
            });

            //
            // start the update timer if not started yet
            //
            if (updateTimer.Enabled == false)
            {
                updateTimer.Enabled = true;
            }
        }

        /// <summary>
        /// Adds the value.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="value">The value.</param>
        public void AddValue(string name, double value)
        {
            if (dataPoints.ContainsKey(name))
                dataPoints[name].Values.Add(value);
        }

        /// <summary>
        /// Adds the values.
        /// </summary>
        /// <param name="values">The values.</param>
        public void AddValues(Dictionary<string, double> values)
        {
            foreach (KeyValuePair<string, double> kvp in values)
                if (dataPoints.ContainsKey(kvp.Key))
                    dataPoints[kvp.Key].Values.Add(kvp.Value);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            //
            // render the grid
            //
            RenderGrid(e.Graphics);

            //
            // render the graph
            //
            RenderGraph(e.Graphics);
        }

        protected void RenderGraph(Graphics graphics)
        {
            //
            // if there are no datapoints render nothing
            //
            if (dataPoints.Count == 0)
                return;

            //
            // render each datapoint
            //
            foreach (KeyValuePair<string, DataPoint> val in dataPoints)
                RenderDataPoint(val.Value, graphics);
        }

        protected void RenderDataPoint(DataPoint dataPoint, Graphics graphics)
        {
            //
            // render nothing if there's nothing to render
            //
            if (dataPoint.Values.Count == 0)
                return;

            Rectangle client = ClientRectangle;
            using (GraphicsPath path = new GraphicsPath())
            {
                //
                // render the graph from right to left - from last value to first;
                // we render POINTS_PER_CELL points per cell - if we run past the left
                // of the available window area then we stop rendering; if we run out
                // of data before hitting the left extreme, well, again, we stop rendering
                //
                int px = client.Right;
                int py = client.Height;
                for (int i = dataPoint.Values.Count - 1; i >= 0; --i)
                {
                    //
                    // if we have fallen over the left edge stop rendering
                    //
                    if (px < 0)
                        break;

                    int y = (int)(dataPoint.NormalValue(dataPoint.Values[i]) * ((double)client.Height / dataPoint.NormalMax));
                    int x = px - PIXELS_PER_POINT;
                    path.AddLine(px, py, x, y);
                    py = y;
                    px = x;
                }
                path.AddLine(px, py, px, client.Height);

                path.CloseFigure();
                graphics.SmoothingMode = SmoothingMode.HighQuality;

                switch (RenderMode)
                {
                    case GraphRenderMode.Stroke:
                        {
                            using (Pen pen = new Pen(dataPoint.Color))
                            {
                                graphics.DrawPath(pen, path);
                            }
                            break;
                        }

                    case GraphRenderMode.Fill:
                        {
                            using (Brush brush = new SolidBrush(dataPoint.Color))
                            {
                                graphics.FillPath(brush, path);
                            }
                            break;
                        }
                }
            }
        }

        protected void RenderGrid(Graphics graphics)
        {
            Rectangle client = ClientRectangle;
            using (Pen pen = new Pen(GridColor))
            {
                for (int x = 0; x < client.Width; x += CELL_SIZE)
                {
                    graphics.DrawLine(pen, x, 0, x, client.Height);
                }

                for (int y = 0; y < client.Height; y += CELL_SIZE)
                {
                    graphics.DrawLine(pen, 0, y, client.Width, y);
                }
            }
        }
    }

    public enum GraphRenderMode
    {
        Stroke,
        Fill
    }

    public class DataPoint
    {
        public string Name { get; set; }
        public Color Color { get; set; }
        public double Min { get; set; }
        public double Max { get; set; }
        public List<double> Values { get; set; }

        public double NormalMin
        {
            get
            {
                return Math.Max(0, Min);
            }
        }

        public double NormalMax
        {
            get
            {
                return NormalMin + (Max - Min);
            }
        }

        public double NormalValue(double value)
        {
            return value + (NormalMin - Min);
        }

        public DataPoint()
        {
            Values = new List<double>();
        }
    }
}
